package org.eclipse.soc.scripting.core;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import javax.script.SimpleScriptContext;

/**
 * Scripting back-end class to execute scripting actions
 * 
 * @author ant
 * 
 */
public class ScriptExecutor {

	private ScriptEngineManager scriptEngineManager;

	private ScriptContext scriptContext;

	private ScriptEngine scriptEngine;

	private Object thiz; // the scripting object

	public ScriptExecutor() {
		scriptEngineManager = new ScriptEngineManager(ScriptBundleActivator.class.getClassLoader());
		scriptContext = new SimpleScriptContext();
		Bindings globals = new SimpleBindings();
		// TODO: put all relevant eclipse related objects to
		// the context so that they could be used in the scripts, a-la eclipse
		// monkey, like:
		// globals.put(workbench, "workbench");
		scriptContext.setBindings(globals, ScriptContext.GLOBAL_SCOPE);
	}

	/**
	 * 
	 * @param clasz
	 */
	public <T> T mirror(Class<T> clasz) {
		return ((Invocable) scriptEngine).getInterface(clasz);
	}

	/**
	 * invoke the required function from the evaluated script
	 * 
	 * @throws NoSuchMethodException
	 * @throws ScriptException
	 */
	public void invokeFunction(String functionName, Object[] args,
			Map<String, Object> map) throws ScriptException,
			NoSuchMethodException {
		setAttributes(map);
		((Invocable) scriptEngine).invokeFunction(functionName, args);
	}

	/**
	 * invoke the required function from the evaluated script <br/> if the
	 * script did not expose an object, then we will try to call a pure function
	 * from the script file
	 * 
	 * @throws NoSuchMethodException
	 * @throws ScriptException
	 * @throws InvocationTargetException 
	 * @throws IllegalAccessException 
	 * @throws IllegalArgumentException 
	 */
	public Object invokeMethod(Class<?> isAssignableClass, Method method, Object[] args, Map<String, Object> map) 
		throws ScriptException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {

		setAttributes(map);
		String methodName = method.getName();
		
		Object result = null;
		if (thiz != null) {
			if (isAssignableClass.isAssignableFrom(thiz.getClass())) {
				result = method.invoke(thiz, args);
			}
			else {
				result = ((Invocable) scriptEngine).invokeMethod(thiz, methodName, args);
			}
		} else {
			result = ((Invocable) scriptEngine).invokeFunction(methodName, args);
		}
		return result;
	}

	/**
	 * invoke the required function from the evaluated script <br/> if the
	 * script did not expose an object, then we will try to call a pure function
	 * from the script file
	 * 
	 * @throws NoSuchMethodException
	 * @throws ScriptException
	 */
	public Object invokeMethod(String functionName, Object[] args, Map<String, Object> map) 
		throws ScriptException, NoSuchMethodException {
		setAttributes(map);

		Object result = null;
		if (thiz != null) {
			result = ((Invocable) scriptEngine).invokeMethod(thiz, functionName, args);
		} else {
			result = ((Invocable) scriptEngine).invokeFunction(functionName, args);
		}
		return result;
	}	
	/**
	 * 
	 * 
	 * @param map
	 */
	public void setParameters(Map<String, Object> map) {
		if (map != null) {
			Bindings bindings = scriptEngine.createBindings();
			bindings.putAll(map);
			scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
			scriptEngine.setContext(scriptContext);
		}
	}

	/**
	 * 
	 * 
	 * @param map
	 */
	public void setAttributes(Map<String, Object> map) {
		if (map != null) {
			Set<String> keys = map.keySet();
			for (Iterator<String> it = keys.iterator(); it.hasNext();) {
				String attributeName = it.next();
				Object attributeValue = map.get(attributeName);
				scriptContext.setAttribute(attributeName, attributeValue,
						ScriptContext.ENGINE_SCOPE);
			}
		}
	}

	/**
	 * 
	 * @see this#evalScript(Reader, String, Map)
	 * 
	 * @param file
	 * @param parameters
	 * @return and object exposed by the script
	 * @throws FileNotFoundException
	 * @throws ScriptException
	 */
	public Object evalScript(File file, Map<String, Object> parameters)
			throws FileNotFoundException, ScriptException {
		return evalScript(new FileReader(file), getFileSuffix(file
				.getAbsolutePath()), parameters);
	}

	/**
	 * Evaluates the provided script
	 * 
	 * @param reader
	 * @param suffix
	 * @param parameters
	 * @return
	 * @throws ScriptException
	 */
	public Object evalScript(Reader reader, String path,
			Map<String, Object> parameters) throws ScriptException {
		scriptEngine = createEngine(path);
		setParameters(parameters);
		thiz = scriptEngine.eval(reader);
		return thiz;
	}

	/**
	 * Creates the ScriptEngine depending on the file suffix.
	 * 
	 * @param path
	 * @return a resolved scripting engine
	 */
	protected ScriptEngine createEngine(String path) {
		String suffix = getFileSuffix(path);
		return scriptEngineManager.getEngineByExtension(suffix);
	}

	/**
	 * Extract the file suffix extension
	 * 
	 * @param path
	 * @return file extension
	 */
	protected String getFileSuffix(String path) {
		int dot = path.lastIndexOf(".") + 1;
		return path.substring(dot);
	}

}
